package org.eraser;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;

import java.util.UUID;
import java.util.function.Consumer;


@Configuration
public class Oauth2CoreConfig {

    /**
     * 登陆页面 uri
     */
    public static final String loginPageUri = "/oauth2/login";

    /**
     * <pre>
     * 用户访问
     * authorizationRequestBaseUri + "/{registrationId}" 时
     * </pre>
     * <pre>
     * DefaultOAuth2AuthorizationRequestResolver
     * 类来构建授权请求, 并将用户重定向到授权服务器的授权端点
     * </pre>
     */
    public static final String authorizationRequestBaseUri = "/oauth2/authorization";

    /**
     * 授权服务器颁发授权码之后 会将浏览器重定向到后端服务  redirectionBaseUri 是限制重定向地址的前缀
     * 你的 yaml 重定向配置要符合 redirectionBaseUri 你需要配置成 :
     * <pre>
     * redirect-uri: "{baseUrl}/enoch/oauth2/callback/{registrationId}"
     * </pre>
     * 这样就是符合 redirectionBaseUri 所限制的格式
     */
    public static final String redirectionBaseUri = "/oauth2/callback/*";

    /**
     * 认证成功后自动跳转的地址
     */
    public static final String loginSuccessUri = "/oauth2/login/success";

    /**
     * 认证失败后自动跳转的地址
     */
    public static final String loginFailureUri = "/oauth2/login/failure";

    /**
     * ClientRegistrationRepository 存储客户端信息。客户端注册信息最终由关联的授权服务器存储和拥有。此类提供了将客户端副本存储在授权服务器外部的能力。
     * 管理客户端注册信息。客户端注册信息包括客户端 ID、客户端密钥、授权类型、重定向 URI 等信息。
     *
     * <p></p>
     * 客户端应用程序可以是用户手机上的app, 也可以是其他类型的应用程序, 您的系统流量有可能不是来自浏览器的流量。
     * ClientRegistrationRepository 存储的是客户端应用程序的信息, 而不是特定于某种类型的应用程序。
     * 只要应用程序使用 OAuth 2.0 或 OpenID Connect 协议与授权服务器进行交互, 它就可以被视为客户端应用程序。
     *
     * <p></p>
     * 在项目启动时
     * OAuth2ClientProperties 自动加载配置 创建ClientRegistration
     * 并实例化一个 ClientRegistrationRepository
     */
    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    /**
     * AuthorizationRequestRepository
     * <pre>
     * 用于在请求之间持久化 OAuth2AuthorizationRequest(授权请求)
     * 持久化授权请求的目的: 将授权请求与授权响应进行关联和验证
     * </pre>
     *
     * <pre>
     * 在授权码授权流程中, 客户端应用程序会将用户重定向到授权服务器以请求用户的授权。
     * 在重定向之前, 客户端应用程序需要构建一个包含必要参数的授权请求 URL
     * 这个过程就是解析授权请求
     * </pre>
     *
     * <pre>
     * 解析出的 OAuth2AuthorizationRequest 会被传递给
     * OAuth2AuthorizationRequestRedirectFilter
     * 后者负责使用这些信息构建授权请求 URL 并将用户重定向到授权服务器
     *
     * 在重定向之前 OAuth2AuthorizationRequestRedirectFilter
     * 会使用 AuthorizationRequestRepository
     * 来持久化解析出的 OAuth2AuthorizationRequest
     * </pre>
     * <p>
     * Oauth2流程
     * <pre>
     * 当用户第一次发起授权请求时 应用程序会将请求信息保存到 AuthorizationRequestRepository
     * 然后 应用程序将用户重定向到授权服务器以获取授权
     *
     * 授权服务器会询问用户是否同意 如果同意 授权服务器会生成授权码并将用户重定向回你的应用程序
     *
     * 当用户被重定向回来时应用程序需要从AuthorizationRequestRepository中加载原始的授权请求
     * 然后 应用程序可以使用这些信息来验证授权服务器返回的授权响应是否与原始请求匹配
     *
     * 这时就使用 AuthorizationRequestRepository
     * 来检索之前持久化的 OAuth2AuthorizationRequest
     *
     * 只有在验证成功后 应用程序才会向授权服务器发起获取令牌的请求
     * </pre>
     */
    @Bean
    public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
        // 把授权请求持久化存入session
        return new HttpSessionOAuth2AuthorizationRequestRepository();
    }

    /**
     * OAuth2AuthorizationRequestResolver 用于解析出 OAuth2AuthorizationRequest(授权请求)
     * <p></p>
     * 在重定向之前,客户端应用程序需要构建一个包含必要参数的授权请求 URL,例如客户端 ID,重定向 URI 和请求的范围。
     * 这个过程就是解析授权请求。
     *
     * <pre>
     * OAuth2AuthorizationRequestResolver接口的实现负责从提供的HttpServletRequest
     * 解析出OAuth2AuthorizationRequest其中包含构建授权请求URL所需的所有信息
     *
     * 解析出的OAuth2AuthorizationRequest
     * 会被传递给OAuth2AuthorizationRequestRedirectFilter
     *
     * 后者负责使用这些信息构建授权请求URL并将用户重定向到授权服务器。
     * </pre>
     */
    @Bean
    public OAuth2AuthorizationRequestResolver authorizationRequestResolver() {
        // 两个入参: 1,客户端存储库 2,授权请求uri (用户访问这个uri时 DefaultOAuth2AuthorizationRequestResolver 类来构建授权请求, 并将用户重定向到授权服务器的授权端点)
        DefaultOAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, authorizationRequestBaseUri);
        defaultAuthorizationRequestResolver.setAuthorizationRequestCustomizer(authorizationRequestCustomizer());
        return defaultAuthorizationRequestResolver;
    }

    /**
     * 修改 OAuth2AuthorizationRequestResolver 构建的 OAuth2AuthorizationRequest (授权请求)
     */
    private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
        return builder -> {
            // 启用PKCE支持 防止CSRF和授权码攻击
            // OAuth2AuthorizationRequestCustomizers.withPkce() 返回一个 Consumer。重写Consumer的accept方法。
            OAuth2AuthorizationRequestCustomizers.withPkce().accept(builder);

            // 在这里添加您自己的定制逻辑
            // builder.additionalParameters(params -> params.put("prompt", "consent"));

            // 添加一个state参数 值为一个随机数 用于防止csrf攻击。
            // builder.additionalParameters(params -> params.put("state", UUID.randomUUID().toString()));
            builder.state(UUID.randomUUID().toString());
        };
    }

    /**
     * 在授权码流程中, 客户端应用程序会在用户授权后收到一个授权码。
     * 客户端应用程序需要使用这个授权码向授权服务器请求访问令牌。
     * DefaultAuthorizationCodeTokenResponseClient 对象负责执行这个请求,
     * 并将响应中的访问令牌和其他信息封装到一个 OAuth2AccessTokenResponse 对象中返回。
     *
     * <p></p>
     * <pre>
     * {@code
     * OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>
     * OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest>
     * OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest>
     * }
     * </pre>
     */
    @Bean
    public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
        // 获取授权码后 拿着授权码向认证服务器去申请令牌
        // 要关注的类 AuthorizationCodeOAuth2AuthorizedClientProvider OAuth2AuthorizationRequestRedirectFilter OAuth2LoginAuthenticationFilter
        // 需要关注的异常 ClientAuthorizationRequiredException
        return new DefaultAuthorizationCodeTokenResponseClient();
    }

    /**
     * 用于存储和检索"已授权客户端"
     *
     * <p>
     * 通常与 AuthorizedClientServiceOAuth2AuthorizedClientManager 一起使用
     * 他们通常使用在 HttpServletRequest 上下文之外
     *
     * <p>
     * 注意如果想从 OAuth2AuthorizedClientService 获取 OAuth2AuthorizedClient
     * 需要在OAuth2client授权成功之后在 AuthenticationSuccessHandler 中实现存储逻辑
     *
     * <p>
     * <pre>
     * {@code
     *  @Bean
     *  public OAuth2AuthorizedClientService authorizedClientService() {
     *      return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
     *  }
     * }
     * </pre>
     */
    @Bean
    public OAuth2AuthorizedClientService authorizedClientService() {
        return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
    }

    /**
     * 跟 OAuth2AuthorizedClientService 一样用于管理"已授权客户端" (OAuth2AuthorizedClient)
     *
     * <p>
     * 持久化授权客户端, 将"访问令牌凭证"与"客户端"和"资源所有者"关联起来。
     */
    @Bean
    public OAuth2AuthorizedClientRepository authorizedClientRepository() {
        return new HttpSessionOAuth2AuthorizedClientRepository();
    }

    /**
     * AuthenticationSuccessHandler 处理认证成功后的逻辑
     */
    @Bean
    public AuthenticationSuccessHandler successHandler() {
        return new SimpleUrlAuthenticationSuccessHandler(loginSuccessUri);
    }

    /**
     * AuthenticationSuccessHandler 处理认证成功后的逻辑
     */
    @Bean
    public AuthenticationFailureHandler failureHandler() {
        return new SimpleUrlAuthenticationFailureHandler(loginFailureUri);
    }

    /**
     * SecurityContext 存 session
     */
    @Bean
    public SecurityContextRepository securityContextRepository() {
        return new HttpSessionSecurityContextRepository();
    }

    /**
     * session 并发控制 .sessionManagement(session -> session.maximumSessions(1))
     */
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }


}